﻿using Padsurface.CoreAudio.Common;
using Padsurface.CoreAudio.Enumerations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace Padsurface.CoreAudio.Wrappers.Model
{
    internal class AudioStreamCapturingState : IAudioStreamState
    {
        private TaskCompletionSource<bool> _taskCompletionSource;

        public AudioStreamCapturingState()
        {
            _taskCompletionSource = new TaskCompletionSource<bool>();
        }

        public Task Initialize(AudioStream context)
        {
            throw new InvalidOperationException();
        }

        public void Start(AudioStream context)
        {
            throw new InvalidOperationException();
        }

        public Task Stop(AudioStream context)
        {
            if (context == null) throw new ArgumentNullException("context");

            context.ContinueCapturing = false;
            context.AudioClient.Stop();

            return _taskCompletionSource.Task;
        }

        public void StartCapturing(AudioStream context)
        {
            if (context == null) throw new ArgumentNullException("context");

            Marshal.ThrowExceptionForHR(context.AudioClient.Start());

            var surpressWarning = Task.Factory
                .StartNew(() => context.ContinueCapturing = true)
                .ContinueWith(t => Record(context));
        }

        private async void Record(AudioStream context)
        {
            var recordInterval = CalculateRecordInterval(context);
            var bytesPerFrame = (context.MixFormat.Channels * context.MixFormat.BitsPerSample / 8);
            var recordBufferSize = (context.BufferSize * bytesPerFrame);

            while (context.ContinueCapturing)
            {
                await Task.Delay(recordInterval);
                CaptureAudioBuffer(context, bytesPerFrame, recordBufferSize);
            }

            context.SetState(new AudioStreamUninitializedState());
            
            _taskCompletionSource.SetResult(true);
        }

        private TimeSpan CalculateRecordInterval(AudioStream context)
        {
            var bufferDuration = (double)(ReferenceTime.PerSecond * context.BufferSize / context.MixFormat.SamplesPerSec);
            var halfBufferDuration = (bufferDuration / ReferenceTime.PerMillisecond / 2);

            return TimeSpan.FromMilliseconds(halfBufferDuration);
        }

        private void CaptureAudioBuffer(AudioStream context, int bytesPerFrame, long recordBufferSize)
        {
            var recordBuffer = new byte[recordBufferSize];
            var recordBufferOffset = 0;

            var numFramesInNextPacket = GetNextPacketSize(context);
            while (numFramesInNextPacket != 0)
            {
                ProcessAvailablePacketInAudioBuffer(context, numFramesInNextPacket, bytesPerFrame, recordBuffer, ref recordBufferOffset);

                numFramesInNextPacket = GetNextPacketSize(context);
            }

            context.OnAudioStreamBufferCaptured(recordBuffer, recordBufferOffset);
        }

        private int GetNextPacketSize(AudioStream context)
        {
            int numFramesInNextPacket;
            Marshal.ThrowExceptionForHR(context.AudioCaptureClient.GetNextPacketSize(out numFramesInNextPacket));

            return numFramesInNextPacket;
        }

        private void ProcessAvailablePacketInAudioBuffer(AudioStream context, int numFramesInNextPacket, int bytesPerFrame, byte[] recordBuffer, ref int recordBufferOffset)
        {
            // Get available packet in buffer
            IntPtr packet;
            uint numFramesToRead;
            AudioClientBufferFlags bufferFlags;
            ulong devicePosition;
            ulong qpcPosition;
            Marshal.ThrowExceptionForHR(
                context.AudioCaptureClient.GetBuffer(out packet, out numFramesToRead, out bufferFlags, out devicePosition, out qpcPosition));

            // Copy available packet from buffer
            var bytesAvailable = (numFramesInNextPacket * bytesPerFrame);
            Marshal.Copy(source: packet, destination: recordBuffer, startIndex: recordBufferOffset, length: bytesAvailable);
            recordBufferOffset += bytesAvailable;

            // Release the packet and make it available for more captured data
            Marshal.ThrowExceptionForHR(context.AudioCaptureClient.ReleaseBuffer(numFramesToRead));
        }
    }
}
